package bblnamed

import (
	"encoding/json"
	"fmt"
	"github.com/boltdb/bolt"
	"path/filepath"
	"time"
)

var (
	dbname      = "metadata.db"
	bucket_name = []byte("metadata")
)

func NewMetadataSystem(workdir string) (MetadataSystem, error) {
	ms := &metadata_system{dbfile: filepath.Join(workdir, dbname), bucket_name: bucket_name}
	if err := ms._init(); err != nil {
		return nil, err
	}
	return ms, nil
}

type Metadata interface {
	Name() string
	Type() int8
	IsObject() bool
	IsLink() bool
	VolumeID() []string
	Link() string
	Size() int64
	ModTime() int64
	CreateTime() int64
	GID() int
	UID() int
	Perm() int
	SetName(name string) error
	SetSize(incr int64) error
	SetGID(gid int) error
	SetUID(uid int) error
	SetPerm(perm int) error
	AddVolumeID(vol string) error
	RemoveVolumeID(vol string) error
	SetLink(link string) error
	Encode() ([]byte, error)
	Decode([]byte) error
}
type metadata struct {
	ms          *metadata_system `json:"-"`
	Sname       string
	Ittype      int8
	Avols       []string
	Slink       string
	Isize       int64
	Imodtime    int64
	Icreatetime int64
	Igid        int
	Iuid        int
	Iperm       int
}

func (m *metadata) Name() string {
	return m.Sname
}
func (m *metadata) Type() int8 {
	return m.Ittype
}
func (m *metadata) IsObject() bool {
	return m.Ittype == 0
}
func (m *metadata) IsLink() bool {
	return m.Ittype == 1
}
func (m *metadata) VolumeID() []string {
	return m.Avols
}
func (m *metadata) Link() string {
	return m.Slink
}
func (m *metadata) Size() int64 {
	return m.Isize
}
func (m *metadata) ModTime() int64 {
	return m.Imodtime
}
func (m *metadata) CreateTime() int64 {
	return m.Icreatetime
}
func (m *metadata) GID() int {
	return m.Igid
}
func (m *metadata) UID() int {
	return m.Iuid
}
func (m *metadata) Perm() int {
	return m.Iperm
}
func (m *metadata) Decode(b []byte) error {
	return json.Unmarshal(b, m)
}

func (m *metadata) Encode() (b []byte, err error) {
	return json.Marshal(m)
}

func (m *metadata) SetName(name string) error {
	m.Sname = name
	m.Imodtime = time.Now().Unix()
	return m.ms.set(m)
}
func (m *metadata) SetSize(size int64) error {
	if size < 0 {
		size = 0
	}
	m.Isize = size
	m.Imodtime = time.Now().Unix()
	return m.ms.set(m)
}
func (m *metadata) SetGID(gid int) error {
	m.Imodtime = time.Now().Unix()
	m.Igid = gid
	return m.ms.set(m)
}
func (m *metadata) SetUID(uid int) error {
	m.Imodtime = time.Now().Unix()
	m.Iuid = uid
	return m.ms.set(m)
}
func (m *metadata) SetPerm(perm int) error {
	m.Imodtime = time.Now().Unix()
	m.Iperm = perm
	return m.ms.set(m)
}

func (m *metadata) AddVolumeID(vol string) error {
	m.Imodtime = time.Now().Unix()
	m.Avols = append(m.Avols, vol)
	return m.ms.set(m)
}

func (m *metadata) RemoveVolumeID(vol string) error {
	m.Imodtime = time.Now().Unix()
	nvols := []string{}
	for _, v := range m.Avols {
		if v != vol {
			nvols = append(nvols, v)
		}
	}
	m.Avols = nvols
	return m.ms.set(m)
}
func (m *metadata) SetLink(link string) error {
	m.Imodtime = time.Now().Unix()
	m.Slink = link
	return m.ms.set(m)
}

type MetadataSystem interface {
	Create(name string, ttype int8) (Metadata, error)
	Find(name string) (Metadata, error)
	FindReal(name string) (Metadata, error)
	Remove(name string) error
	Close() error
}

type metadata_system struct {
	dbfile      string
	db          *bolt.DB
	bucket_name []byte
}

func (m *metadata_system) _init() error {
	db, err := bolt.Open(m.dbfile, 0600, &bolt.Options{Timeout: 30 * time.Second})
	if err != nil {
		return err
	}
	m.db = db
	return m.db.Update(func(tx *bolt.Tx) error {
		_, err := tx.CreateBucketIfNotExists(m.bucket_name)
		return err
	})

}
func (m *metadata_system) Create(name string, ttype int8) (Metadata, error) {
	if ttype != 0 && ttype != 1 {
		return nil, fmt.Errorf("error ttype %v", ttype)
	}
	tx, err := m.db.Begin(true)
	if err != nil {
		return nil, err
	}
	defer tx.Rollback()
	createtime := time.Now().Unix()
	meta := &metadata{Sname: name, Ittype: ttype, Slink: name, Avols: make([]string, 0), Icreatetime: createtime, Imodtime: createtime}
	val, err := meta.Encode()
	if err != nil {
		return nil, err
	}
	meta.ms = m
	if err = tx.Bucket(m.bucket_name).Put([]byte(name), val); err != nil {
		return nil, err
	}

	if err := tx.Commit(); err != nil {
		return nil, err
	}
	return meta, nil
}
func (m *metadata_system) Find(name string) (Metadata, error) {
	tx, err := m.db.Begin(false)
	if err != nil {
		return nil, err
	}
	defer tx.Rollback()
	val := tx.Bucket(m.bucket_name).Get([]byte(name))
	if val == nil || len(val) == 0 {
		return nil, fmt.Errorf("%v not found", name)
	}
	meta := new(metadata)
	if err = meta.Decode(val); err != nil {
		return nil, err
	}
	meta.ms = m
	return meta, nil
}
func (m *metadata_system) FindReal(name string) (Metadata, error) {
	tx, err := m.db.Begin(false)
	if err != nil {
		return nil, err
	}
	defer tx.Rollback()
	val := tx.Bucket(m.bucket_name).Get([]byte(name))
	if val == nil || len(val) == 0 {
		return nil, fmt.Errorf("%v not found", name)
	}
	meta := new(metadata)
	if err = meta.Decode(val); err != nil {
		return nil, err
	}
	if meta.IsLink() {
		return m.FindReal(meta.Link())
	}
	meta.ms = m
	return meta, nil
}
func (m *metadata_system) Remove(name string) error {
	tx, err := m.db.Begin(true)
	if err != nil {
		return err
	}
	defer tx.Rollback()
	if err = tx.Bucket(m.bucket_name).Delete([]byte(name)); err != nil {
		return err
	}
	if err := tx.Commit(); err != nil {
		return err
	}
	return nil
}
func (m *metadata_system) Close() error {
	return m.db.Close()
}

func (m *metadata_system) set(meta Metadata) error {
	tx, err := m.db.Begin(true)
	if err != nil {
		return err
	}
	defer tx.Rollback()
	val, err := meta.Encode()
	if err != nil {
		return err
	}
	if err = tx.Bucket(m.bucket_name).Put([]byte(meta.Name()), val); err != nil {
		return err
	}

	if err := tx.Commit(); err != nil {
		return err
	}
	return nil
}
